Skip to content

perf(gated_delta_net): fold q/k L2-norm into the gated_delta_rule kernel - #5396

Open
yuchenwang3 wants to merge 2 commits into
NVIDIA:mainfrom
yuchenwang3:fix/gdn-qk-l2norm-in-kernel
Open

perf(gated_delta_net): fold q/k L2-norm into the gated_delta_rule kernel#5396
yuchenwang3 wants to merge 2 commits into
NVIDIA:mainfrom
yuchenwang3:fix/gdn-qk-l2norm-in-kernel

Conversation

@yuchenwang3

@yuchenwang3 yuchenwang3 commented Jun 17, 2026

Copy link
Copy Markdown

What

Move GatedDeltaNet q/k L2 normalization into the FLA gated_delta_rule kernel instead of materializing normalized q/k before the call.

The current GDN and GDN2 implementations share _prepare_input_for_gated_delta_rule, but GDN2's kernel path does not enable in-kernel normalization. The rebased change therefore makes deferral an explicit helper option:

  • GDN defers normalization and passes use_qk_l2norm_in_kernel=self.use_qk_l2norm.
  • GDN2 keeps its existing caller-side normalization and continues to pass False to its kernel.

This preserves GDN2 behavior added after the original PR rather than silently removing its normalization during the rebase.

Why

Caller-side normalization leaves an additional pre-split normalized query_key activation live for backward alongside the q/k tensors the gated-delta kernel already needs. Letting FLA own normalization removes that extra caller-side activation and keeps the normalization state with the kernel-owned q/k, which saves activation memory in long-context GDN-heavy hybrids.

The numerical contract is unchanged:

  • both caller-side FLA l2norm and the in-kernel path use eps=1e-6;
  • normalization is per head over the last dimension, so GQA head replication does not change the result;
  • use_qk_l2norm=False remains a no-op.

Validation

The original change ran in Qwen3.5-35B-A3B 128K SFT on 16×B200. The current-main replay passes Black, isort, Ruff, Pylint, py_compile, and git diff --check.

The final 398a42b4 head was also exercised with an 8-rank torchrun on 8×B200:

  • the GDN/GDN2 kernel flag-vs-explicit-normalization parity test passed on every rank;
  • the BF16 end-to-end GatedDeltaNet.forward comparison passed on every rank, covering outputs, input gradients, all parameter gradients, and both routing flags;
  • all 12 parameterized deterministic-mode GDN/GDN2 cases passed on every rank.

@copy-pr-bot

copy-pr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@yuchenwang3
yuchenwang3 marked this pull request as ready for review July 10, 2026 01:39
@yuchenwang3
yuchenwang3 requested review from a team as code owners July 10, 2026 01:39
@svcnvidia-nemo-ci
svcnvidia-nemo-ci requested a review from a team July 10, 2026 01:40
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Jul 10, 2026
@yuchenwang3

Copy link
Copy Markdown
Author

@xuantengh pinging you as the most recent committer on gated_delta_net.py. This folds the explicit q/k l2norm into the FLA gated_delta_rule kernel via use_qk_l2norm_in_kernel=True, dropping a materialized intermediate at long context; it ran in real Qwen3.5-35B-A3B hybrid training at 128K. One-file change, open since Jun 17 — could you review or /ok to test?

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Aug 6, 2026
@xuantengh

Copy link
Copy Markdown
Contributor

Thanks for the contribution! We had a refactor to the GDN module recently in #5843, would you please rebase that and apply this to the new file?

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 6, 2026
@yuchenwang3

Copy link
Copy Markdown
Author

Done, @xuantengh — merged latest main and re-applied the change onto the refactored layout from #5843:

  • common.py: the explicit l2norm() in _GDNBase._prepare_input_for_gated_delta_rule is removed (replaced by a NOTE explaining the fold and the eps=1e-6 equivalence).
  • gdn.py: the gated_delta_rule call in GatedDeltaNet.forward now passes use_qk_l2norm_in_kernel=self.use_qk_l2norm.

Net diff vs main is the same two-line semantic change as before (+7/−4 with the comment block). Ready for another look.

@yuchenwang3

Copy link
Copy Markdown
Author

@xuantengh one more follow-up before I refresh this again: current main added GDN2 sharing _prepare_input_for_gated_delta_rule. I've kept GDN2's caller-side normalization and only defer GDN to the kernel. Does that split match what you'd expect? If so I'll update the branch and request CI.

@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch from b752055 to 39fd9a0 Compare August 25, 2026 07:43
@yuchenwang3

Copy link
Copy Markdown
Author

Rebased onto current main and adjusted the shared input helper added since the original PR: GDN now defers q/k normalization to its kernel, while GDN2 keeps the existing caller-side normalization. Local formatting, syntax, and routing checks pass. @xuantengh, could you re-review this head when you get a chance?

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Aug 25, 2026
@xuantengh

Copy link
Copy Markdown
Contributor

Sorry for the delayed reply. Would you please add a test to tests/unit_tests/ssm/test_gated_delta_net.py to validate toggling use_qk_l2norm can produce on-par results?

Defer GDN q/k normalization to the gated delta rule kernel so the kernel owns normalization and caller autograd no longer retains an additional pre-split normalized query_key activation alongside the kernel q/k. Keep caller-side normalization as the default for GDN2, which shares the input-preparation helper but does not enable this path.

Signed-off-by: Yuchen Wang <yw.yy953e@alibaba-inc.com>
@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch from 39fd9a0 to 8206dc3 Compare August 25, 2026 10:36
FLA l2norm normalizes the last dimension and does not accept a dim argument. Use its default eps=1e-6 API in the deterministic GDN and GDN2 fallbacks.

Add a BF16 end-to-end regression that compares the former caller-normalized route with kernel-owned normalization across outputs, input gradients, and parameter gradients, while asserting the helper and kernel routing flags.

Signed-off-by: Yuchen Wang <yw.yy953e@alibaba-inc.com>
@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch from 8206dc3 to 398a42b Compare August 25, 2026 10:37
@yuchenwang3

Copy link
Copy Markdown
Author

Done, @xuantengh. I added a small kernel-level parity check plus a BF16 end-to-end test that runs the real GatedDeltaNet.forward path. The latter compares the old caller-normalized route with kernel-owned normalization across outputs, input gradients, and every parameter gradient, and also asserts that both the helper and kernel receive the in-kernel flag and that raw q/k reach the kernel.

The new tests pass on all 8 ranks on 8×B200. While running the existing deterministic matrix I also caught and fixed a current FLA API mismatch in the newly activated fallback (l2norm has no dim argument); all 12 deterministic GDN/GDN2 cases now pass on every rank. The final head is 398a42b4 and DCO is green. Could you take another look and run /ok to test 398a42b4af753c76ffeeb14198d9b41a1bb2de1a when you get a chance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants